/*
 * Copyright (c) 2012, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.bmi.gwt.mi.client.channel;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.bmi.gwt.mi.client.events.ConnectionHandler;
import org.bmi.gwt.mi.client.impl.ProxyFactoryClient;
import org.bmi.gwt.mi.client.impl.WrapperFactoryClient;
import org.bmi.gwt.mi.shared.BroadChannel;
import org.bmi.gwt.mi.shared.Channel;
import org.bmi.gwt.mi.shared.callbacks.AttributeCallback;
import org.bmi.gwt.mi.shared.callbacks.BindCallback;
import org.bmi.gwt.mi.shared.callbacks.Callback;
import org.bmi.gwt.mi.shared.communication.AttributeMessage;
import org.bmi.gwt.mi.shared.communication.AttributeMessage.MsgType;
import org.bmi.gwt.mi.shared.communication.CallsRequestMessage;
import org.bmi.gwt.mi.shared.communication.ErrorMessage;
import org.bmi.gwt.mi.shared.communication.FreeQueueMessage;
import org.bmi.gwt.mi.shared.communication.FreeQueueMessage.FreeMsgType;
import org.bmi.gwt.mi.shared.communication.ListenQueueMessage;
import org.bmi.gwt.mi.shared.communication.ListenQueueMessage.CreateMsgType;
import org.bmi.gwt.mi.shared.communication.Message;
import org.bmi.gwt.mi.shared.communication.attribute.AttributeValue;
import org.bmi.gwt.mi.shared.communication.call.Call;
import org.bmi.gwt.mi.shared.exceptions.NameParseException;
import org.bmi.gwt.mi.shared.util.Constants;
import org.bmi.gwt.mi.shared.util.ProxyFactory;
import org.bmi.gwt.mi.shared.util.UtilityFunctions;
import org.bmi.gwt.mi.shared.util.WrapperFactory;
import org.bmi.gwt.ws.client.JsWebSocket;
import org.bmi.gwt.ws.client.WsConnection;
import org.bmi.gwt.ws.client.WsConnection.WsEventHandler;

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.Cookies;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.SerializationException;

/**
 * Object which opens the WebSocket connection, giving access to tool
 * functionality from client side..
 * 
 * @author marco.tamburelli@gmail.com
 */
public final class Connection
{
	private static int CALLBACK = 0;
	private static final Map<Integer, Callback> CALLBACKS_MAP = new HashMap<Integer, Callback>();

	private static Connection INSTANCE;

	protected final MiSerializer serializer = GWT.create(MiSerializer.class);

	protected final ProxyFactory proxyFactory = GWT.create(ProxyFactoryClient.class);
	protected final WrapperFactory wrapperFactory = GWT.create(WrapperFactoryClient.class);

	private WsConnection<Message> ws;
	private ConnectionHandler connectionHandler;

	private HashMap<String, ChannelClient> queuesToChannels;

	public interface MiSerializer extends org.bmi.gwt.ws.shared.PayloadSerializer<Message>
	{}

	private Connection(ConnectionHandler connectionHandler, String credentials)
	{
		if (!JsWebSocket.isSupported())
		{
			connectionHandler.onException(new UnsupportedOperationException("WebSocket is not supported."));
			return;
		}

		/* Defining expiration date for credentials cookie */
		Date expire = new Date();
		expire.setTime(1000 * 60 * 60 * 24 + expire.getTime());

		/* The domain will be the host name */
		String domain = Window.Location.getHostName();

		/* If the host name is not a valid domain name it will be nullified */
		if (!domain.contains(".")) domain = null;

		/* Providing credentials as cookie */
		Cookies.setCookie(Constants.CREDENTIALS_COOKIE_NAME, credentials, expire, domain, "/", false);

		this.connectionHandler = connectionHandler;
		this.queuesToChannels = new HashMap<String, ChannelClient>();

		this.ws = new WsConnection<Message>(this.serializer, Constants.BMI_BASE_URL + Constants.SERVICE_ENDPOINT);
		this.ws.setEventHandler(new WsEventHandler<Message>()
		{
			@Override
			public void onOpen()
			{
				Connection.this.connectionHandler.onConnectionBegin(Connection.this);
			}

			@Override
			public void onClose()
			{
				/* All defined channels will be disconnected from the server */
				for (ChannelClient channel : Connection.this.queuesToChannels.values())
					channel.disconnect();
			}

			@Override
			public void onError(Exception ex)
			{
				fireError(ex);
			}

			@Override
			public void onObject(Message msg)
			{
				String queryExpr = msg.getQueueExpression();
				final ChannelClient channel = Connection.this.queuesToChannels.get(queryExpr);

				if (channel == null)
					fireError(new Exception("A channel to the \"" + queryExpr
							+ "\" queue was never defined on this client."));
				else if (msg instanceof CallsRequestMessage)
				{
					try
					{
						Connection.this.queuesToChannels.get(queryExpr).onResponse(
								((CallsRequestMessage) msg).getCalls());
					}
					catch (Exception e)
					{
						onError(e);
					}
				}
				else if (msg instanceof ListenQueueMessage
						&& ((ListenQueueMessage) msg).getCreateMsgType() == CreateMsgType.Listening)
				{
					BindCallback callback = (BindCallback) CALLBACKS_MAP.remove(((ListenQueueMessage) msg)
							.getCallback());

					if (callback != null)
					{
						callback.onBound(channel);
						channel.connect();
					}
				}
				else if (msg instanceof FreeQueueMessage
						&& ((FreeQueueMessage) msg).getFreeMsgType() == FreeMsgType.Freed)
				{
					channel.disconnect();
				}
				else if (msg instanceof AttributeMessage)
				{
					AttributeMessage<? extends AttributeValue<?>> attrMsg = (AttributeMessage<?>) msg;

					switch (attrMsg.getMsgType())
					{
					case Removed:
					case Updated:
					case ValueProvided:
						@SuppressWarnings("unchecked")
						AttributeCallback<AttributeValue<?>> callback = (AttributeCallback<AttributeValue<?>>) CALLBACKS_MAP
								.remove(attrMsg.getCallback());

						callback.onSuccess(attrMsg.getValue());
						break;
					default: /* Nothing should be done */
					}
				}
				else if (msg instanceof ErrorMessage)
				{
					ErrorMessage errMsg = (ErrorMessage) msg;

					Callback callback = CALLBACKS_MAP.remove(errMsg.getCallback());
					callback.onError(errMsg.getException());

					/* the error makes impossible to bind any longer */
					channel.disableBind();
				}
			}
		});
	}

	private Connection(ConnectionHandler connectionHandler)
	{
		this(connectionHandler, "RANDOM_GENERATED_" + UtilityFunctions.generateUniqueId());
	}

	/**
	 * Creates and returns a connection.
	 * 
	 * Note that only one connection per client can be made available, several
	 * call to this method or
	 * {@link #createConnection(ConnectionHandler, String)} will return the same
	 * object.
	 * 
	 * @param connectionHandler Handler for events concerning connection.
	 * @return
	 */
	public static Connection createConnection(ConnectionHandler connectionHandler)
	{
		if (INSTANCE == null) INSTANCE = new Connection(connectionHandler);

		return INSTANCE;
	}

	/**
	 * Creates and returns a connection.
	 * 
	 * Note that only one connection per client can be made available, several
	 * call to this method or {@link #createConnection(ConnectionHandler)} will
	 * return the same object.
	 * 
	 * @param connectionHandler Handler for events concerning connection.
	 * @param credentials Credentials used in case server side provides a login
	 *        mechanism.
	 * @return
	 */
	public static Connection createConnection(ConnectionHandler connectionHandler, String credentials)
	{
		if (INSTANCE == null) INSTANCE = new Connection(connectionHandler, credentials);

		return INSTANCE;
	}

	void fireError(Exception ex)
	{
		this.connectionHandler.onException(ex);
	}

	/**
	 * Returns a {@link Channel} implementation suitable for client management.
	 * The returned channel in case of disconnection will continue to be
	 * available for a short amount of time. If in that time the queue will be
	 * not owned by the current client or another one, it will be discarded.
	 * 
	 * @param queueName The name of required queue.
	 * @return The requested queue.
	 * @throws NameParseException
	 */
	public Channel getChannel(String queueName) throws NameParseException
	{
		return getChannel(queueName, false);
	}

	/**
	 * Returns a {@link Channel} implementation suitable for client management.
	 * 
	 * @param queueName The name of required queue.
	 * @param discardOnDisconnect When this flag is set to <code>true</code> the
	 *        queue will be discarded when the connection is closed, or somehow
	 *        interrupted. Normally a queue will not be immediately discarded,
	 *        but will stay available for a short amount of time.
	 * @return The requested queue.
	 * @throws NameParseException
	 */
	public Channel getChannel(String queueName, boolean discardOnDisconnect) throws NameParseException
	{
		if (!queueName.matches("((\\w+)\\.)*(\\w+)")) throw new NameParseException("Invalid queue name: " + queueName);

		if (this.queuesToChannels.containsKey(queueName))
			return this.queuesToChannels.get(queueName);
		else
		{
			ChannelClient q = new ChannelClient(queueName, this.proxyFactory, this.wrapperFactory, this,
					discardOnDisconnect);
			this.queuesToChannels.put(queueName, q);

			return q;
		}
	}

	/**
	 * Returns a {@link BroadChannel} implementation suitable for client
	 * management.
	 * 
	 * @param queueExpr The expression of the set of requested queuesToChannels.
	 *        Note that the expression must identify a pool: must be of the form
	 *        <code>org.test.*</code>
	 * @return The requested queue expression.
	 * @throws NameParseException
	 */
	public BroadChannel getBroadChannel(String queueExpr) throws NameParseException
	{
		if (!queueExpr.matches("((\\w+)\\.)*(\\*)"))
			throw new NameParseException("Invalid queue expression: " + queueExpr);

		BroadChannelClient q = new BroadChannelClient(queueExpr, this.proxyFactory, this);

		return q;
	}

	/**
	 * This method publishes the provided array of calls.
	 * 
	 * @param queueExpression
	 * @param calls
	 */
	void publish(String queueExpression, List<Call> calls)
	{
		CallsRequestMessage publishRequest = new CallsRequestMessage();
		publishRequest.setCalls(calls);
		publishRequest.setQueueExpression(queueExpression);

		try
		{
			this.ws.send(publishRequest);
		}
		catch (SerializationException e)
		{
			fireError(e);
		}
	}

	void listen(String queueName, HashMap<String, AttributeValue<?>> attributes, boolean discardOnDisconnect,
			BindCallback callback)
	{
		int cbIdx = CALLBACK++;
		CALLBACKS_MAP.put(cbIdx, callback);

		ListenQueueMessage qMsg = new ListenQueueMessage();
		qMsg.setCallback(cbIdx);
		qMsg.setQueueExpression(queueName);
		qMsg.setAttributes(attributes);
		qMsg.setCreateMsgType(CreateMsgType.ListenRequest);
		qMsg.setDiscardOnDisconnect(discardOnDisconnect);

		try
		{
			this.ws.send(qMsg);
		}
		catch (SerializationException e)
		{
			fireError(e);
		}
	}

	<T extends AttributeValue<?>> void setQueueAttribute(String queueName, String key, T value,
			AttributeCallback<T> callback)
	{
		int cbIdx = CALLBACK++;
		CALLBACKS_MAP.put(cbIdx, callback);

		AttributeMessage<T> aMsg = new AttributeMessage<T>();
		aMsg.setCallback(cbIdx);
		aMsg.setQueueExpression(queueName);

		aMsg.setKey(key);
		aMsg.setValue(value);
		aMsg.setMsgType(MsgType.RequestUpdate);

		try
		{
			this.ws.send(aMsg);
		}
		catch (SerializationException e)
		{
			fireError(e);
		}
	}

	<T extends AttributeValue<?>> void getQueueAttribute(String queueName, String key, AttributeCallback<T> callback)
	{
		int cbIdx = CALLBACK++;
		CALLBACKS_MAP.put(cbIdx, callback);

		AttributeMessage<T> aMsg = new AttributeMessage<T>();
		aMsg.setCallback(cbIdx);
		aMsg.setQueueExpression(queueName);

		aMsg.setKey(key);
		aMsg.setMsgType(MsgType.RequestValue);

		try
		{
			this.ws.send(aMsg);
		}
		catch (SerializationException e)
		{
			fireError(e);
		}
	}

	<T extends AttributeValue<?>> void removeQueueAttribute(String queueName, String key, AttributeCallback<T> callback)
	{
		int cbIdx = CALLBACK++;
		CALLBACKS_MAP.put(cbIdx, callback);

		AttributeMessage<T> aMsg = new AttributeMessage<T>();
		aMsg.setCallback(cbIdx);
		aMsg.setQueueExpression(queueName);

		aMsg.setKey(key);
		aMsg.setMsgType(MsgType.RequestRemove);

		try
		{
			this.ws.send(aMsg);
		}
		catch (SerializationException e)
		{
			fireError(e);
		}
	}

	void free(String queueName, boolean discard)
	{
		FreeQueueMessage qMsg = new FreeQueueMessage();
		qMsg.setQueueExpression(queueName);
		qMsg.setFreeMsgType(FreeMsgType.FreeRequest);
		qMsg.setDiscard(discard);

		try
		{
			this.ws.send(qMsg);
		}
		catch (SerializationException e)
		{
			fireError(e);
		}
	}
}
